﻿namespace FxCopContrib
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text.RegularExpressions;
    using Microsoft.FxCop.Sdk;
    using System.Globalization;

    /// <summary>
    /// This rule checks the values passed to static methods of Regex, the Regex constructor and 
    /// expressions used for DataAnnotations or EntLib Validations
    /// </summary>
    /// <remarks>
    /// This rule might result in false positives if a string value is changed under certain conditions
    /// before being passed to the Regex.
    /// 
    /// Todo: Add support for ASP.NET Regular Expression validators
    /// </remarks>
    public class RegularExpressionDoesNotCompile : RuleBase
    {
        /// <summary>
        /// 
        /// </summary>
        public RegularExpressionDoesNotCompile() : base("RegularExpressionDoesNotCompile") { }

        private Dictionary<Node, List<object>> readonlyOrConstValues = new Dictionary<Node, List<object>>();
        private Dictionary<Node, object> localValues;

        /// <summary>
        /// 
        /// </summary>
        /// <param name="type"></param>
        /// <returns></returns>
        public override ProblemCollection Check(TypeNode type)
        {
            var constructors = type.Members.Where(m => m.NodeType == NodeType.InstanceInitializer
                || m.NodeType == NodeType.StaticInitializer);
            foreach (var member in constructors)
            {
                Visit(member);
            }


            var other = type.Members.Where(m => m.NodeType != NodeType.InstanceInitializer
                && m.NodeType != NodeType.StaticInitializer);
            foreach (var member in other)
            {
                Visit(member);
            }

            return this.Problems;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="field"></param>
        public override void VisitField(Field field)
        {
            if (field.Attributes != null)
            {
                CheckDataAnnotation(field.Attributes);
            }

            base.VisitField(field);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="attributes"></param>
        private void CheckDataAnnotation(AttributeNodeCollection attributes)
        {
            foreach (var att in attributes)
            {
                if (att.Type.FullName == typeof(System.ComponentModel.DataAnnotations.RegularExpressionAttribute).FullName
                    || att.Type.FullName == "Microsoft.Practices.EnterpriseLibrary.Validation.RegexValidator")
                {
                    var arg = att.GetPositionalArgument(0);
                    string expression = (string)((Literal)arg).Value;
                    TestRegex(expression, RegexOptions.None, arg);
                }
            }
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="parameter"></param>
        public override void VisitParameter(Parameter parameter)
        {
            if (parameter.Attributes != null)
            {
                CheckDataAnnotation(parameter.Attributes);
            }
            base.VisitParameter(parameter);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="property"></param>
        public override void VisitProperty(PropertyNode property)
        {
            if (property.Attributes != null)
            {
                CheckDataAnnotation(property.Attributes);
            }
            base.VisitProperty(property);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="method"></param>
        public override void VisitMethod(Method method)
        {
            localValues = new Dictionary<Node, object>();
            if (method.Locals != null)
            {
                foreach (var local in method.Locals)
                {
                    localValues.Add(local, null);
                }
            }
            base.VisitMethod(method);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="assignment"></param>
        public override void VisitAssignmentStatement(AssignmentStatement assignment)
        {
            // readonly / constant string asssignments to string field
            // only assignment to string local
            if (assignment.Target.NodeType == NodeType.Local)
            {
                if (localValues.ContainsKey(assignment.Target))
                {
                    if (assignment.Source.NodeType == NodeType.Literal
                        && localValues[assignment.Target] == null)
                    {
                        localValues[assignment.Target] = ((Literal)assignment.Source).Value;
                    }
                    else
                    {
                        localValues.Remove(assignment.Target);
                    }
                }
            }

            else if (assignment.Target.NodeType == NodeType.MemberBinding)
            {
                MemberBinding binding = (MemberBinding)assignment.Target;
                Field field = binding.BoundMember as Field;
                if (field != null && (field.IsInitOnly || field.IsLiteral))
                {
                    if (assignment.Source.NodeType == NodeType.Literal)
                    {
                        if (!readonlyOrConstValues.ContainsKey(field))
                        {
                            readonlyOrConstValues.Add(field, new List<object>());
                        }
                        readonlyOrConstValues[field].Add(((Literal)assignment.Source).Value);
                    }
                }
            }

            // Assignments 
            base.VisitAssignmentStatement(assignment);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="call"></param>
        public override void VisitMethodCall(MethodCall call)
        {
            // Check calls to Regex.Match, Regex.Replace, Regex.Matches
            MemberBinding binding = call.Callee as MemberBinding;
            if (binding.BoundMember.DeclaringType != null
                && binding.BoundMember.DeclaringType.FullName == typeof(Regex).FullName
                && binding.BoundMember.IsStatic)
            {
                int expressionPosition = -1;
                int optionPosition = -1;

                switch (binding.BoundMember.Name.Name)
                {
                    case "IsMatch":
                    case "Match":
                    case "Matches":
                    case "Split":
                        expressionPosition = 1;
                        if (call.Operands.Count == 3)
                            optionPosition = 2;
                        break;
                    case "Replace":
                        expressionPosition = 2;
                        if (call.Operands.Count > 3)
                            optionPosition = call.Operands.Count;
                        break;
                    default:
                        break;
                }

                IEnumerable<string> expression = ExtractValue<string>(call.Operands, expressionPosition);
                IEnumerable<RegexOptions> options = ExtractValue<RegexOptions>(call.Operands, optionPosition);

                TestRegex(expression, options, call);
            }

            base.VisitMethodCall(call);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="construct"></param>
        public override void VisitConstruct(Construct construct)
        {
            if (construct.Type.FullName == typeof(Regex).FullName)
            {
                var operands = construct.Operands;
                IEnumerable<string> expression = ExtractValue<string>(operands, 0);
                IEnumerable<RegexOptions> options = ExtractValue<RegexOptions>(operands, 1);

                TestRegex(expression, options, construct);
            }

            //Check calls to new Regex

            base.VisitConstruct(construct);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="operands"></param>
        /// <param name="index"></param>
        /// <returns></returns>
        private IEnumerable<T> ExtractValue<T>(ExpressionCollection operands, int index)
        {
            if (index < 0 || index >= operands.Count)
            {
                return new T[] { default(T) };
            }

            else if (operands[index].NodeType == NodeType.Literal)
            {
                return new T[] { (T)((Literal)operands[index]).Value };
            }

            else if (operands[index].NodeType == NodeType.Local)
            {
                return new T[] { (T)localValues[operands[index]] };
            }

            else if (operands[index].NodeType == NodeType.MemberBinding)
            {
                MemberBinding binding = (MemberBinding)operands[index];
                Field field = binding.BoundMember as Field;
                if (field != null && (field.IsInitOnly || field.IsLiteral))
                {
                    if (readonlyOrConstValues.ContainsKey(field))
                    {
                        return readonlyOrConstValues[field].Cast<T>();
                    }
                }
            }

            return new T[] { default(T) };
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="expressions"></param>
        /// <param name="options"></param>
        /// <param name="node"></param>
        private void TestRegex(IEnumerable<string> expressions, IEnumerable<RegexOptions> options, Node node)
        {
            foreach (string expression in expressions)
            {
                foreach (RegexOptions option in options)
                {
                    TestRegex(expression, option, node);
                }
            }
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="expression"></param>
        /// <param name="option"></param>
        /// <param name="node"></param>
        private void TestRegex(string expression, RegexOptions option, Node node)
        {
            if (string.IsNullOrWhiteSpace(expression))
                return;

            if (!TestRegexOptions(option, node))
                return;

            try
            {
                Regex.IsMatch("", expression);
            }
            catch (System.ArgumentException ex)
            {
                //   System.ArgumentException:
                //     A regular expression parsing error occurred.
                this.Problems.Add(new Problem(GetNamedResolution("InvalidRegex", expression, ex.Message), node));
            }
        }

        /// <summary>
        /// Test for invalid RegexOptions
        /// </summary>
        private bool TestRegexOptions(RegexOptions options, Node node)
        {
            var allOptions = Enum.GetValues(typeof(RegexOptions)).OfType<RegexOptions>().Aggregate((p, q) => p | q);

            if (!allOptions.HasFlag(options))
            {
                // the bitwise combination contains a undefined options
                var undefined = (int)options & ~(int)allOptions;
                this.Problems.Add(new Problem(GetNamedResolution("UnknownRegexOptions", undefined), node));
                return false;
            }
            else if (options.HasFlag(RegexOptions.ECMAScript))
            {

                if (options.HasFlag(RegexOptions.CultureInvariant)
                    || options.HasFlag(RegexOptions.ExplicitCapture)
                    || options.HasFlag(RegexOptions.IgnorePatternWhitespace)
                    || options.HasFlag(RegexOptions.RightToLeft)
                    || options.HasFlag(RegexOptions.Singleline))
                {
                    this.Problems.Add(new Problem(GetNamedResolution("InvalidRegexOptions"), node));
                    return false;
                }
            }
            return true;
        }

    }
}
